Batching (effect)
https://effect.website/docs/batching/
gpt-5.icon
🧱 ステップごとの解説
1. モデルとAPI関数を定義する
まず普通に、User, Todo モデルとエラー型を定義し、API 関数を Effect.tryPromise で実装します。
code:ts
const getUserById = (id: number) =>
Effect.tryPromise({
try: () =>
fetch(https://api.example.demo/getUserById?id=${id}).then(res =>
res.json() as Promise<User>
),
catch: () => new GetUserError()
})
このままだと getUserById を複数回呼ぶたびに API が何度も叩かれます。
2. リクエストを構造化して宣言する(Request)
Effect では、API呼び出しを 「リクエスト」オブジェクト として明示的に表現します。
code:ts
interface GetUserById extends Request.Request<User, GetUserError> {
readonly _tag: "GetUserById"
readonly id: number
}
const GetUserById = Request.tagged<GetUserById>("GetUserById")
これにより、Effect は「同じリクエスト(例: id=1)」を識別・比較できるようになります。
Request.Request
Request.tagged
3. リクエストをどう解決するかを定義する(RequestResolver)
RequestResolver はリクエストを実際に実行する層です。
特に RequestResolver.makeBatched を使うことで、複数の同型リクエストをまとめて処理できます。
code:ts
const GetUserByIdResolver = RequestResolver.makeBatched(
(requests: ReadonlyArray<GetUserById>) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/getUserByIdBatch", {
method: "POST",
body: JSON.stringify({ users: requests.map(({ id }) => ({ id })) })
}).then(res => res.json()) as Promise<Array<User>>,
catch: () => new GetUserError()
}).pipe(
Effect.andThen(users =>
Effect.forEach(requests, (req, i) =>
Request.completeEffect(req, Effect.succeed(usersi!))
)
)
)
)
こうして、[{id:1}, {id:2}, {id:3}] のような複数のリクエストを一度に処理できます。
4. クエリ関数を定義する(Effect.request)
リクエストとリゾルバを結びつけて使えるようにします。
code:ts
const getUserById = (id: number) =>
Effect.request(GetUserById({ id }), GetUserByIdResolver)
これにより、Effect が内部的に同種のリクエストを自動的にバッチングします
こういうイメージ?mrsekut.icon
Request (Tag的ななにか)
Resolver (Impl的ななにか)
Effect.requestでそれを結びつける (Layerみたいなイメージか)
5. 実際に使う(例:Todoの所有者全員に通知)
code:ts
const program = Effect.gen(function* () {
const todos = yield* getTodos
yield* Effect.forEach(todos, (todo) => notifyOwner(todo), {
batching: true // ← これが重要!
})
})
これにより、全 todo に対して getUserById を呼んでも
内部的には1回のAPI呼び出し(バッチ)で処理 されます。
6. コンテキスト付きリゾルバ
RequestResolver は Context(依存サービス)を使うこともできます。
例として、HttpService を依存に持たせることも可能です。
code:ts
class HttpService extends Context.Tag("HttpService")<HttpService, { fetch: typeof fetch }>() {}
const GetTodosResolver = RequestResolver.fromEffect((_: GetTodos) =>
Effect.andThen(HttpService, (http) =>
Effect.tryPromise({
try: () => http.fetch("..."),
catch: () => new GetTodosError()
})
)
).pipe(RequestResolver.contextFromServices(HttpService))
7. キャッシュ(Effect.withRequestCaching)
同じリクエストを繰り返し行う場合、キャッシュを有効化できます。
code:ts
const getUserById = (id: number) =>
Effect.request(GetUserById({ id }), GetUserByIdResolver).pipe(
Effect.withRequestCaching(true)
)
これにより、同じユーザーIDの取得はキャッシュから返されます。
8. カスタムキャッシュ(Request.makeCache)
アプリ全体や一部で独自キャッシュを設定することも可能。
code:ts
Effect.provide(
Layer.setRequestCache(
Request.makeCache({ capacity: 256, timeToLive: "60 minutes" })
)
)
Requests & Batching
Effect.blocked
Effect.cacheRequestResult
Effect.request
Effect.runRequestBlock
Effect.step
Effect.withRequestBatching
Effect.withRequestCache
Effect.withRequestCaching